function testLog() {
    baseLog("/////////////////////////////////////////////////////////////////");
    baseLog("Hello chapter-8.js!");
}

function testObject() {
    {
        let obj1 = new Object();
        obj1.name = 'name1';
        obj1.age = 29;
        obj1.sayName = function() {
            baseLog(this.name);
        }
        let obj2 = {
            name: 'name2',
            age: 18,
            sayName() {
                baseLog(this.name);
            }
        };
        let {name, age} = obj2;
        baseLog(name);                  // 'name2'
        baseLog(age);                   // 18
    }
    {
        let person = {};
        Object.defineProperty(person, 'name', {
            writable: false,
            configurable: false,
            value: 'nic'
        });
        baseLog(person.name);                   // 'nic'
        person.name = 'rim';                        // due to writable: false
        delete person.name;                         // due to configurable: false
        baseLog(person.name);                   // 'nic'
    }
    {
        let book = {};
        Object.defineProperties(book, {
            year_: {
                writable: true,
                value: 2017
            },
            edition: {
                writable: true,
                value: 1
            },
            year: {
                get() {
                    return this.year_;
                },
                set(newValue) {
                    if (newValue > 2017) {
                        this.year_ = newValue;
                        this.edition += newValue - 2017;
                    }
                }
            }
        });
        book.year = 2019;
        baseLog(book.edition);                  // 3
        baseLog(book.year);                     // 2019
        let descriptor = Object.getOwnPropertyDescriptor(book, 'year_');
        baseLog(descriptor);                    // {value: 2019, writable: true, enumerable: false, configurable: false}
        baseLog(typeof descriptor.get);         // undefined
        descriptor = Object.getOwnPropertyDescriptor(book, 'year');
        baseLog(descriptor);                    // {enumerable: false, configurable: false, get: ƒ, set: ƒ}
        baseLog(typeof descriptor.get);         // function
        baseLog(Object.getOwnPropertyDescriptors(book));        // {year_: {…}, edition: {…}, year: {…}}
    }
    {
        let dest, src, result;
        dest = {};
        src = {id: 'src'};
        result = Object.assign(dest, src);
        baseLog(dest === result);                   // true
        baseLog(dest === src);                      // false
        baseLog(dest);                              // {id: 'src'}
        dest = {};
        result = Object.assign(dest, {id: 'src1', a: 'foo'}, {id: 'src2', b: 'bar'});
        baseLog(dest);                              // {id: 'src2', a: 'foo', b: 'bar'}
        dest = {
            set a(val) {
                baseLog(`Invoked dest setter with param ${val}`);
            }
        };
        src = {
            get a() {
                baseLog('Invoked src getter');
                return 'foo';
            }
        };
        Object.assign(dest, src);
        baseLog(dest);                  // Invoked src getter, Invoked dest setter with param foo, {}
    }
    {
        let name = 'chris';
        let person = {
            name
        };
        baseLog(person);                // {name: 'chris'}
    }
}

function testCreateObject() {
    {
        function Person(name, age, job) {
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function() {
                baseLog(this.name);
            };
        }
        let person1 = new Person('name1', 30, 'SDE');
        let person2 = new Person('name2', 40, 'Doctor');
        person1.sayName();                      // 'name1'
        person2.sayName();                      // 'name2'
        baseLog(person1.constructor == Person);     // true
        baseLog(person1 instanceof Object);         // true
        baseLog(person1 instanceof Person);         // true
        Person('name3', 50, 'God');
        window.sayName();                       // 'name3'
        let o = new Object();
        Person.call(o, 'name4', 60, 'Student');
        o.sayName();                            // 'name4'
    }
    {
        let Person = function() {};
        Person.prototype.name = '';
        Person.prototype.age = 0;
        Person.prototype.job = '';
        Person.prototype.sayName = function() {
            baseLog(this.name);
        };
        let person1 = new Person();
        let person2 = new Person();
        baseLog(person1.sayName == person2.sayName);        // true
    }
    {
        function Person() {};
        baseLog(typeof Person.prototype);                   // Object
        baseLog(Person.prototype);                          // {constructor: f Person(), __proto__: Object}
        baseLog(Person.prototype.constructor == Person);    // true
        baseLog(Person.prototype.__proto__ === Object.prototype);       // true
        baseLog(Person.prototype.__proto__.constructor === Object);     // true
        baseLog(Person.prototype.__proto__.__proto__ === null);         // true
        /**
         * {
         *  constructor: f Object(),
         *  toString: ...
         *  hasOwnerProperty: ...
         *  isPrototypeOf: ...
         *  ...
         * }
         */
        baseLog(Person.prototype.__proto__);
        let person1 = new Person(),
            person2 = new Person();
        baseLog(person1.__proto__ === Person.prototype);                // true
        baseLog(person1.__proto__.constructor === Person);              // true
        baseLog(person1.__proto__ === person2.__proto__);               // true
        baseLog(Person.prototype.isPrototypeOf(person1));               // true
        baseLog(Object.getPrototypeOf(person1) == Person.prototype);    // true
    }
    {
        let biped = {
            numLegs: 2
        };
        let person = {
            name: 'name'
        };
        Object.setPrototypeOf(person, biped);
        baseLog(person.name);               // 'name'
        baseLog(person.numLegs);            // 2
        baseLog(Object.getPrototypeOf(person) === biped);   // true
    }
    {
        function hasPrototypeProperty(object, name) {
            return !object.hasOwnProperty(name) && (name in object);
        }
        function Person() {}
        Person.prototype.name = 'pro_name';
        Person.prototype.age = 30;
        let p1 = new Person();
        let p2 = new Person();
        baseLog(p1.hasOwnProperty('name'));         // false
        p1.name = 'obj_name';
        baseLog(p1.hasOwnProperty('name'));         // true
        baseLog('name' in p1);                      // true
        baseLog(hasPrototypeProperty(p1, 'name'));  // false
        baseLog(p1.name);                           // 'obj_name'
        baseLog(p2.name);                           // 'pro_name'
        p1.name = null;
        baseLog(p1.hasOwnProperty('name'));         // true
        baseLog('name' in p1);                      // true
        baseLog(hasPrototypeProperty(p1, 'name'));  // false
        baseLog(p1.name);                           // null
        delete p1.name;
        baseLog(p1.hasOwnProperty('name'));         // false
        baseLog('name' in p1);                      // true
        baseLog(hasPrototypeProperty(p1, 'name'));  // true
        baseLog(p1.name);                           // 'pro_name'
        baseLog(Object.keys(Person.prototype));     // ['name', 'age']
    }
    {
        function Person() {};
        let p = new Person();
        Person.prototype.sayHi = function() {
            baseLog('hi');
        };
        p.sayHi();                                      // 'hi'
    }
    {
        function Person() {};
        let p = new Person();
        Person.prototype = {
            constructor: Person,
            sayHi() {
                baseLog('hi');
            }
        };
        // p.sayHi();                                      // Uncaught TypeError: p.sayHi is not a function
    }
    {
        function Person() {};
        Person.prototype = {
            constructor: Person,
            name: 'name',
            friends: ['f1', 'f2']
        };
        let p1 = new Person();
        let p2 = new Person();
        p1.friends.push('f3');
        baseLog(p2.friends);                            // ['f1', 'f2', 'f3']
    }
}

function testInherit() {
    {
        function SuperType() {
            this.property = true;
        }
        SuperType.prototype.getSuperValue = function() {
            return this.property;
        };
        function SubType() {
            this.subproperty = false;
        }
        SubType.prototype = new SuperType();
        SubType.prototype.getSubValue = function() {
            return this.subproperty;
        };
        let instance = new SubType();
        baseLog(instance.getSuperValue());              // true
        baseLog(instance.getSubValue());                // false
        SubType.prototype.getSuperValue = function() {
            return false;
        }
        baseLog(instance.getSuperValue());              // false
    }
    {
        {
            function SuperType() {
                this.arr = [1,2];
            }
            function SubType() {};
            SubType.prototype = new SuperType();
            let ins1 = new SubType();
            ins1.arr.push(3);
            let ins2 = new SubType();
            baseLog(ins2.arr);                              // [1,2,3]
        }
        {
            function SuperType() {
                this.arr = [1,2];
            }
            function SubType() {
                SuperType.call(this);
            }
            let ins1 = new SubType();
            ins1.arr.push(3);
            let ins2 = new SubType();
            baseLog(ins2.arr);                              // [1,2]
        }
        {
            function SuperType(name) {
                this.name = name;
                this.arr = [1,2];
            }
            SuperType.prototype.sayName = function() {
                baseLog(this.name);
            };
            function SubType(name, age) {
                SuperType.call(this, name);
                this.age = age;
            }
            SubType.prototype = new SuperType();
            SubType.prototype.sayAge = function() {
                baseLog(this.age);
            };
            let ins1 = new SubType('tina', 18);
            ins1.arr.push(3);
            ins1.sayName();                             // 'tina'
            ins1.sayAge();                              // 18
            let ins2 = new SubType('lucy', 22);
            baseLog(ins2.arr);                      // [1,2]
            ins2.sayName();                             // 'lucy'
            ins2.sayAge();                              // 22
        }
    }
}

function testClass() {
    {
        class Animal {};
        class Person {
            constructor() {
                baseLog('Person constructor');
            }
        };
        class Vegetable {
            constructor() {
                this.color = 'orange';
            }
        };
        let a = new Animal();
        let p = new Person();           // 'Person constructor'
        let v = new Vegetable();
        baseLog(v.color);           // 'orange'
        baseLog(Animal);            // class Animal {}
        baseLog(typeof Animal);     // function
        baseLog(Person.prototype);  // {constructor: ƒ}
        baseLog(Person === Person.prototype.constructor);   // true
    }
    {
        class Person {
            constructor(name) {
                baseLog(arguments.length);
                this.name = name || null;
            }
        }
        let p1 = new Person;            // 0
        baseLog(p1.name);           // null
        let p2 = new Person();          // 0
        baseLog(p2.name);           // null
        let p3 = new Person('tina');    // 1
        baseLog(p3.name);           // 'tina'
    }
    {
        class Person {
            constructor(override) {
                this.foo = 'foo';
                if (override) {
                    return {
                        bar: 'bar'
                    };
                }
            }
        }
        let p1 = new Person(),
            p2 = new Person(true);
        baseLog(p1);                    // Person {foo: 'foo'}
        baseLog(p2);                    // {bar: 'bar'}
        baseLog(p1 instanceof Person);  // true
        baseLog(p2 instanceof Person);  // false
    }
    {
        class Person {
            constructor() {
                this.locate = () => baseLog('instance', this);
            }
            locate() {
                baseLog('prototype', this);
            }
            static locate() {
                baseLog('class', this);
            }
        }
        let p = new Person();
        p.locate();                 // instance Person {locate: ƒ}
        Person.prototype.locate();  // prototype {constructor: ƒ, locate: ƒ}
        Person.locate();            // class class Person {...}
    }
    {
        class Person {
            constructor() {
                this.arr = [1,2,3];
            }
            [Symbol.iterator]() {
                return this.arr.entries();
            }
        }
        let p = new Person();
        for (let [idx, val] of p) {
            baseLog(idx, val);          // 0 1, 1 2, 2 3
        }
    }
    {
        class Vehicle {
            identifyPrototype(id) {
                baseLog(id, this);
            }
            static identifyClass(id) {
                baseLog(id, this);
            }
        }
        class Bus extends Vehicle {}
        let v = new Vehicle();
        let b = new Bus();
        b.identifyPrototype('bus');             // bus Bus {}
        v.identifyPrototype('vehicle');         // vehicle Vehicle {}
        Bus.identifyClass('bus');               // bus class Bus extends Vehicle {}
        Vehicle.identifyClass('vehicle');       // vehicle class Vehicle {...}
    }
    {
        class Vehicle {
            constructor() {
                this.hasEngine = true;
            }
            static identify() {
                baseLog('vehicle');
            }
        }
        class Bus extends Vehicle {
            constructor() {
                // Do not use 'this' before calling 'super()', otherwise will throw 'ReferenceError'.
                super();        // The same as super.constructor()
                baseLog(this instanceof Vehicle);      // true;
                baseLog(this);                    // Bus {hasEngine: true}
            }
            static identify() {
                super.identify();
            }
        }
        new Bus();
        Bus.identify();         // 'vehicle'
    }
    {
        // abstract virtual class
        class Vehicle {
            constructor() {
                if (new.target === Vehicle) {
                    throw new Error('Vehicle cannot be directly instantiated');
                }
                if (!this.foo) {
                    throw new Error('Inheriting class must define foo()');
                }
                baseLog('success!');
            }
        }
        class Bus extends Vehicle {
            foo() {}
        }
        class Van extends Vehicle {}
        // new Vehicle();          // Uncaught Error: Vehicle cannot be directly instantiated
        // new Van();              // Uncaught Error: Inheriting class must define foo()
        new Bus();                 // success!
    }
}


///////////////////////////////////////////////////////////////////////////////////////
testLog();
testObject();
testCreateObject();
testInherit();
testClass();
